// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/stat.h>

#include <blobfs/format.h>
#include <digest/digest.h>
#include <digest/merkle-tree.h>
#include <fbl/algorithm.h>
#include <fbl/function.h>
#include <fbl/string.h>
#include <fbl/string_buffer.h>
#include <fbl/string_printf.h>
#include <fbl/unique_fd.h>
#include <fs-management/mount.h>
#include <fs-test-utils/fixture.h>
#include <fs-test-utils/perftest.h>
#include <perftest/perftest.h>
#include <unittest/unittest.h>

#include <utility>

namespace {

using digest::Digest;
using digest::MerkleTree;
using fs_test_utils::Fixture;
using fs_test_utils::FixtureOptions;
using fs_test_utils::PerformanceTestOptions;
using fs_test_utils::TestCaseInfo;
using fs_test_utils::TestInfo;

// Supported read orders for this benchmark.
enum class ReadOrder {
  // Blobs are read in the order they were written
  kSequentialForward,
  // Blobs are read in the inverse order they were written
  kSequentialReverse,
  // Blobs are read in a random order
  kRandom,
};

// An in-memory representation of a blob.
struct BlobInfo {
  // Path to the generated blob.
  fbl::StringBuffer<fs_test_utils::kPathSize> path;

  fbl::unique_ptr<char[]> merkle;
  size_t size_merkle;

  fbl::unique_ptr<char[]> data;
  size_t size_data;
};

// Describes the parameters of the test case.
struct BlobfsInfo {
  // Total number of blobs in blobfs.
  ssize_t blob_count;

  // Size in bytes of each blob in BlobFs.
  size_t blob_size;

  // Path to every blob in Blobfs
  fbl::Vector<fbl::StringBuffer<fs_test_utils::kPathSize>> paths;

  // Order in which to read the blobs from blobfs.
  fbl::Vector<uint64_t> path_index;
};

// Helper for streaming operations (such as read, write) which may need to be
// repeated multiple times.
template <typename T, typename U>
inline int StreamAll(T func, int fd, U* buf, size_t max) {
  size_t n = 0;
  while (n != max) {
    ssize_t d = func(fd, &buf[n], max - n);
    if (d < 0) {
      return -1;
    }
    n += d;
  }
  return 0;
}

// Get a readable name for a given number of bytes.
fbl::String GetNameForSize(size_t size_in_bytes) {
  static const char* const kUnits[] = {"bytes", "Kbytes", "Mbytes", "Gbytes"};
  size_t current_unit = 0;
  size_t current_size = size_in_bytes;
  size_t size;
  while (current_unit < fbl::count_of(kUnits) &&
         current_size >= (1u << (10 * (current_unit + 1)))) {
    current_size = current_size / (1 << 10 * current_unit);
    ++current_unit;
  }

  size = (size_in_bytes >> (10 * current_unit));
  return fbl::StringPrintf("%lu%s", size, kUnits[current_unit]);
}

fbl::String GetNameForOrder(ReadOrder order) {
  switch (order) {
    case ReadOrder::kSequentialForward:
      return "Sequential";
    case ReadOrder::kSequentialReverse:
      return "Reverse";
    case ReadOrder::kRandom:
      return "Random";
  }

  return "";
}

// Creates a an in memory blob.
bool MakeBlob(fbl::String fs_path, size_t blob_size, unsigned int* seed,
              fbl::unique_ptr<BlobInfo>* out) {
  BEGIN_HELPER;
  // Generate a Blob of random data
  fbl::AllocChecker ac;
  fbl::unique_ptr<BlobInfo> info(new (&ac) BlobInfo);
  EXPECT_EQ(ac.check(), true);
  info->data.reset(new (&ac) char[blob_size]);
  EXPECT_EQ(ac.check(), true);
  // rand_r produces a cyclic sequence, in order to avoid hitting that cap
  // and generating identical blobs, we avoid consuming an element of the
  // sequence for each byte. We did hit this issue, which translates into
  // test failures.
  unsigned int initial_seed = rand_r(seed);
  for (size_t i = 0; i < blob_size; i++) {
    info->data[i] = static_cast<char>(rand_r(&initial_seed));
  }
  info->size_data = blob_size;

  // Generate the Merkle Tree
  info->size_merkle = MerkleTree::GetTreeLength(blob_size);
  if (info->size_merkle == 0) {
    info->merkle = nullptr;
  } else {
    info->merkle.reset(new (&ac) char[info->size_merkle]);
    ASSERT_EQ(ac.check(), true);
  }
  Digest digest;
  ASSERT_EQ(MerkleTree::Create(&info->data[0], info->size_data, &info->merkle[0], info->size_merkle,
                               &digest),
            ZX_OK, "Couldn't create Merkle Tree");
  fbl::StringBuffer<sizeof(info->path)> path;
  path.AppendPrintf("%s/", fs_path.c_str());
  digest.ToString(path.data() + path.size(), path.capacity() - path.size());
  strcpy(info->path.data(), path.c_str());

  // Sanity-check the merkle tree
  ASSERT_EQ(MerkleTree::Verify(&info->data[0], info->size_data, &info->merkle[0], info->size_merkle,
                               0, info->size_data, digest),
            ZX_OK, "Failed to validate Merkle Tree");

  *out = std::move(info);
  END_HELPER;
}

// Returns a path within the fs such that it is a valid blobpath.
// The generated path is 'root_path/0....0'.
fbl::String GetNegativeLookupPath(const fbl::String& fs_path) {
  fbl::String negative_path =
      fbl::StringPrintf("%s/%*d", fs_path.c_str(), static_cast<int>(2 * Digest::kLength), 0);
  return negative_path;
}

class BlobfsTest {
 public:
  BlobfsTest(BlobfsInfo&& info) : info_(std::move(info)) {}

  // Measure how much time each of the operations in the Fs takes, for a known size.
  // First we add as many blobs as we need to, and then, we proceed to execute each operation.
  bool ApiTest(perftest::RepeatState* state, Fixture* fixture) {
    BEGIN_HELPER;

    // How many blobs do we need to add.
    fbl::unique_ptr<BlobInfo> new_blob;

    for (int64_t curr = 0; curr < info_.blob_count; ++curr) {
      MakeBlob(fixture->fs_path(), info_.blob_size, fixture->mutable_seed(), &new_blob);
      fbl::unique_fd fd(open(new_blob->path.c_str(), O_CREAT | O_RDWR));
      ASSERT_TRUE(fd, strerror(errno));
      ASSERT_EQ(ftruncate(fd.get(), info_.blob_size), 0, strerror(errno));
      ASSERT_EQ(StreamAll(write, fd.get(), new_blob->data.get(), new_blob->size_data), 0,
                strerror(errno));
      info_.paths.push_back(new_blob->path);
      info_.path_index.push_back(curr);
      new_blob.reset();
    }

    fbl::AllocChecker ac;
    fbl::unique_ptr<char[]> buffer(new (&ac) char[info_.blob_size]);
    ASSERT_TRUE(ac.check());

    state->DeclareStep("generate_blob");
    state->DeclareStep("create");
    state->DeclareStep("truncate");
    state->DeclareStep("write");
    state->DeclareStep("close_write_fd");
    state->DeclareStep("open");
    state->DeclareStep("read");
    state->DeclareStep("unlink");
    state->DeclareStep("close_read_fd");

    // At this specific state, measure how much time in average it takes to perform each of the
    // operations declared.
    while (state->KeepRunning()) {
      MakeBlob(fixture->fs_path(), info_.blob_size, fixture->mutable_seed(), &new_blob);
      state->NextStep();

      fbl::unique_fd fd(open(new_blob->path.c_str(), O_CREAT | O_RDWR));
      ASSERT_TRUE(fd);
      state->NextStep();

      ASSERT_EQ(ftruncate(fd.get(), info_.blob_size), 0);
      state->NextStep();

      ASSERT_EQ(StreamAll(write, fd.get(), new_blob->data.get(), info_.blob_size), 0,
                "Failed to write Data");
      // Force pending writes to be sent to the underlying device.
      ASSERT_EQ(fsync(fd.get()), 0);
      state->NextStep();

      ASSERT_EQ(close(fd.release()), 0);
      state->NextStep();

      fd.reset(open(new_blob->path.c_str(), O_RDONLY));
      ASSERT_TRUE(fd);
      state->NextStep();

      ASSERT_EQ(StreamAll(read, fd.get(), &buffer[0], info_.blob_size), 0);
      ASSERT_EQ(memcmp(buffer.get(), new_blob->data.get(), new_blob->size_data), 0);
      state->NextStep();

      unlink(new_blob->path.c_str());
      ASSERT_EQ(fsync(fd.get()), 0);

      state->NextStep();
      ASSERT_EQ(close(fd.release()), 0);
    }
    END_HELPER;
  }

  // After doing the API test, we use the written blobs to measure, lookup, negative-lookup
  // read
  bool ReadTest(ReadOrder order, perftest::RepeatState* state, Fixture* fixture) {
    BEGIN_HELPER;
    state->DeclareStep("lookup");
    state->DeclareStep("read");
    state->DeclareStep("negative_lookup");
    ASSERT_EQ(info_.path_index.size(), info_.paths.size());
    ASSERT_GT(info_.path_index.size(), 0);
    SortPathsByOrder(order, fixture->mutable_seed());

    fbl::AllocChecker ac;
    fbl::unique_ptr<char[]> buffer(new (&ac) char[info_.blob_size]);
    ASSERT_TRUE(ac.check());

    uint64_t current = 0;
    fbl::String negative_path = GetNegativeLookupPath(fixture->fs_path());

    while (state->KeepRunning()) {
      size_t path_index = info_.path_index[current % info_.paths.size()];
      fbl::unique_fd fd(open(info_.paths[path_index].c_str(), O_RDONLY));
      ASSERT_TRUE(fd);
      state->NextStep();
      ASSERT_EQ(StreamAll(read, fd.get(), &buffer[0], info_.blob_size), 0);
      state->NextStep();
      fbl::unique_fd no_fd(open(negative_path.c_str(), O_RDONLY));
      ASSERT_FALSE(no_fd);
      ++current;
    }
    END_HELPER;
  }

 private:
  void SortPathsByOrder(ReadOrder order, unsigned int* seed) {
    switch (order) {
      case ReadOrder::kSequentialForward:
        for (uint64_t curr = 0; curr < info_.paths.size(); ++curr) {
          info_.path_index[curr] = curr;
        }
        break;

      case ReadOrder::kSequentialReverse:
        for (uint64_t curr = 0; curr < info_.paths.size(); ++curr) {
          info_.path_index[curr] = info_.paths.size() - curr - 1;
        }
        break;

      case ReadOrder::kRandom:
        int64_t swaps = info_.paths.size();
        while (swaps > 0) {
          size_t src = rand_r(seed) % info_.paths.size();
          size_t target = rand_r(seed) % info_.paths.size();
          info_.path_index[src] = info_.path_index[target];
          info_.path_index[target] = info_.path_index[src];
          --swaps;
        }
        break;
    }
  }

  BlobfsInfo info_;
};

bool RunBenchmark(int argc, char** argv) {
  FixtureOptions f_opts = FixtureOptions::Default(DISK_FORMAT_BLOBFS);
  PerformanceTestOptions p_opts;
  // 30 Samples for each operation at each stage.
  constexpr uint32_t kSampleCount = 100;
  const size_t blob_sizes[] = {
      128,          // 128 b
      128 * 1024,   // 128 Kb
      1024 * 1024,  // 1 MB
  };
  const size_t blob_counts[] = {
      10,
      100,
      1000,
      10000,
  };
  const ReadOrder orders[] = {
      ReadOrder::kSequentialForward,
      ReadOrder::kSequentialReverse,
      ReadOrder::kRandom,
  };

  if (!fs_test_utils::ParseCommandLineArgs(argc, argv, &f_opts, &p_opts)) {
    return false;
  }

  fbl::Vector<TestCaseInfo> testcases;
  fbl::Vector<BlobfsTest> blobfs_tests;
  size_t test_index = 0;
  for (auto blob_size : blob_sizes) {
    for (auto blob_count : blob_counts) {
      // Skip the largest blob size/count combination because it
      // increases the overall running time too much.
      if (blob_size >= 1024 * 1024 && blob_count >= 10000) {
        continue;
      }
      BlobfsInfo fs_info;
      fs_info.blob_count = (p_opts.is_unittest) ? 1 : blob_count;
      fs_info.blob_size = blob_size;
      blobfs_tests.push_back(std::move(fs_info));
      TestCaseInfo testcase;
      testcase.teardown = false;
      testcase.sample_count = kSampleCount;

      fbl::String size = GetNameForSize(blob_size);

      TestInfo api_test;
      api_test.name = fbl::StringPrintf("%s/%s/%luBlobs/Api", disk_format_string_[f_opts.fs_type],
                                        size.c_str(), blob_count);
      // There should be enough space for each blob, the merkle tree nodes, and the inodes.
      api_test.required_disk_space =
          blob_count * (blob_size + 2 * MerkleTree::kNodeSize + blobfs::kBlobfsInodeSize);
      api_test.test_fn = [test_index, &blobfs_tests](perftest::RepeatState* state,
                                                     fs_test_utils::Fixture* fixture) {
        return blobfs_tests[test_index].ApiTest(state, fixture);
      };
      testcase.tests.push_back(std::move(api_test));

      if (blob_count > 0) {
        for (auto order : orders) {
          TestInfo read_test;
          read_test.name =
              fbl::StringPrintf("%s/%s/%luBlobs/Read%s", disk_format_string_[f_opts.fs_type],
                                size.c_str(), blob_count, GetNameForOrder(order).c_str());
          read_test.test_fn = [test_index, order, &blobfs_tests](perftest::RepeatState* state,
                                                                 fs_test_utils::Fixture* fixture) {
            return blobfs_tests[test_index].ReadTest(order, state, fixture);
          };
          read_test.required_disk_space =
              blob_count * (blob_size + 2 * MerkleTree::kNodeSize + blobfs::kBlobfsInodeSize);
          testcase.tests.push_back(std::move(read_test));
        }
      }
      testcases.push_back(std::move(testcase));
      ++test_index;
    }
  }

  return fs_test_utils::RunTestCases(f_opts, p_opts, testcases);
}

}  // namespace

int main(int argc, char** argv) {
  return fs_test_utils::RunWithMemFs([argc, argv]() { return RunBenchmark(argc, argv) ? 0 : -1; });
}
